/*
 * Copyright (c) 2011-2012 Alexander Dubu
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * o  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * o  Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * o  Neither the name Axil nor the names of its contributors may be used to
 *    endorse or promote products derived from this software without specific
 *    prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package axil.stdlib.collection.type;

import axil.api.AxilObject;
import axil.api.AxilType;
import axil.etc.Cloned;
import axil.etc.ConditionallyImmutable;
import axil.framework.collection.Pair;
import axil.framework.type.Added;
import axil.framework.type.Subtracted;
import axil.stdlib.core.type.Nil;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import static axil.framework.Functions.*;


/**
 * An Axil implementation of a map or dictionary.
 */
public class Dict<K extends AxilObject, T extends AxilObject>
	extends AxilCollection<T>
	implements ConditionallyImmutable<Dict<K, T>>, Added, Subtracted, Keyed, Cloned<Dict<K,T>> {
    public static final AxilType type = Dict_Type.object;
	public static final Dict empty = new Dict().immute();

	private HashMap<K,T> map;
	private boolean immutable;

    // empty constructor required for reflection
    public Dict() {
        map = new HashMap<K, T>();
    }

	public Dict(Pair<K, T>... values) {
		map = new HashMap<K,T>();
		for (Pair<K,T> p : values) {
			map.put(p.k(), p.v());
		}
	}


	public Dict(Map other) {
		map = new HashMap<K,T>();
		for (Object key : map.keySet()) {
			K k = (K)axil(key);
			T v = (T)axil(other.get(key));
			map.put(k, v);
		}
	}


	private Dict(Dict<K,T> other) {
		map = new HashMap<K,T>();
		for (Entry<K,T> e : other.map.entrySet()) {
			map.put(e.getKey(), e.getValue());
		}
	}

    public AxilObject put(AxilObject object) {
        if (object == Nil.object) {
            return Nil.object;
        }
        if (object instanceof Dict) {
            Dict result = this;
            Dict other = (Dict)object;
            if (immutable) {
                result = this.cloned();
            }
            for (Object nextKey : other.map.keySet()) {
                result.map.put((K) nextKey, (T) other.map.get(nextKey));
            }
            return result;
        }
        if (object instanceof Pair) {
            Dict result = this;
            Pair addPair = (Pair)object;
            if (immutable) {
                result = this.cloned();
            }
            return result.at(addPair.k(), addPair.v());
        }
        throw error(axil(), "invalid-operand",
                nv("operator", "-"), nv("type", object.type()));
    }


	/**
	 * Get the type metadata for this object. The type metadata contains
	 * information about the type itself, as an object.
	 */
	public AxilType type() {
		return type;
	}


	public AxilCollection<K> keys() {
		return new Arry(map.keySet()).immute();
	}


	/**
	 * Get the values for this collection. For most collections, this simply
	 * returns this object, but for complex collections such as dictionary, it
	 * returns the values.
	 */
	public AxilCollection<T> values() {
		return new Arry(map.values()).immute();
	}


	/**
	 * Tell if this collection contains the value (not the key) given.
	 */
	public boolean has(AxilObject object) {
		return map.containsValue(object);
	}


	/**
	 * Clone this object, returning a new object. The clone is sufficiently
	 * deep to ensure that any modifications to the clone, or to any member of
	 * the clone, do not impact this object.
	 */
	public Dict<K, T> cloned() {
		return new Dict(this);
	}


	/**
	 * Tell if this object is equivalent to the given object. The object given
	 * is never null. The object given may be of any type of value object.  If
	 * the given object is not a suitable type for comparison, a
	 * ClassCastException may be thrown.
	 */
    public boolean equalTo(AxilObject object) {
		if (object instanceof AxilCollection) {
			AxilCollection o = (AxilCollection)object;
			if (this.size() == o.size()) {
				if (o instanceof Dict) {
					return this.map.equals(((Dict)o).map);
				}
				return new Arry(this.map.values()).equals(o);
			}
		}
		return false;
	}


	/**
	 * Return the key where the given object resides within this collection.
	 * A nil value is returned if the object does not live in this collection.
	 */
	public AxilObject keyof(AxilObject object) {
		for (Entry<K,T> e : map.entrySet()) {
			if (e.getValue().equals(object)) {
				return e.getKey();
			}
		}
		return Nil.object;
	}


	/**
	 * Return the object at the given key, or nil if the key is not contained
	 * within this object.
	 */
	public AxilObject at(AxilObject key) {
		T v = map.get(key);
		return v == null ? Nil.object : v;
	}


	public Dict<K,T> at(K key, T value) {
		edit();
		map.put(key, value);
		return this;
	}


    /**
     * Add the given value to this value, returning a new object. The given
     * value is never null or nil. The value may be a numeric type of any other
     * type of object that can sensibly implement the Added interface.
     */
    public AxilObject add(AxilObject object) {
        AxilObject result = put(object);
        return result instanceof Dict ? ((Dict) result).immute() : result;
    }


    /**
     * Subtract the given object from this object. The given value is never
     * null or nil. The value may be a numeric type of any other type of object
     * that can sensibly implement the Subtracted interface.
     */
    public AxilObject subtract(AxilObject object) {
        if (object == Nil.object) {
            return Nil.object;
        }
        // Remove by key (ignoring value)
        if (object instanceof Dict) {
            Dict result = this;
            Dict other = (Dict)object;
            if (immutable) {
                result = this.cloned();
            }
            for (Object nextKey : other.map.keySet()) {
                result.map.remove((K) nextKey);
            }
            return result.immute();
        }
        if (object instanceof Pair) {
            Dict result = this;
            Pair removePair = (Pair)object;
            if (immutable) {
                result = this.cloned();
            }
            result.map.remove(removePair.k());
            return result.immute();
        }
        // Remove all keys
        if (object instanceof Iterable) {
            Dict result = this;
            if (immutable) {
                result = this.cloned();
            }
            for (Object nextKey : (Iterable)object) {
                result.map.remove((K) nextKey);
            }
            return result.immute();
        }
        // Remove one key
        Dict result = this;
        if (immutable) {
            result = this.cloned();
        }
        result.map.remove(object);
        return result.immute();
    }


    /**
	 * Mark this object as immutable, preventing any further changes. Any
	 * attempt to alter this collection after this point results in an exception.
	 */
	public Dict<K, T> immute() {
		immutable = true;
		return this;
	}


	private void edit() {
		if (immutable) {
            throw error(axil(), "immutable-object");
        }
	}


	/**
	 * Get the size of this collection.
	 */
	public int size() {
		return map.size();
	}


	/**
	 * Returns an iterator over a set of elements of type T. The iterator
	 * returned does not allow for alteration of this collection.
	 */
	public Iterator<T> iterator() {
		return new Safe<T>(map.values().iterator());
	}


	/**
	 * Return the host application (or built-in Java) object that most closely
	 * matches this value object. Return returned value is never null unless
	 * this value object represents the special 'nil' object.
	 */
	public Object intrinsic() {
		Map m = new HashMap(map.size());
		for (Entry<K,T> e : map.entrySet()) {
			m.put(e.getKey().intrinsic(), e.getValue().intrinsic());
		}
		return m;
	}


	/**
	 * Return a string representation of this object. The string returned is
	 * never null.
	 */
	public String toString() {
		if (map.isEmpty()) {
			return "";
		}
		StringBuilder b = new StringBuilder();
		for (K k : map.keySet()) {
			T val = map.get(k);
			if (b.length() > 0) {
                b.append(Dict_Type.PAIR_SEPARATOR);
                b.append(' ');
            }
			b.append(k.toString());
            b.append(' ');
            b.append(Dict_Type.KEY_VALUE_SEPARATOR);
            b.append(' ');
            b.append(val.toString());
		}
		return b.toString();
	}
}
